package chloroform.memento

import scaladuct._
import asm.JavaBean
import java.util.ArrayList
import chloroform.{TypedProjectEntry, Project, ConfigPageRegister}
import util.logging.Logged
import java.lang.String
import javax.validation.constraints.NotNull

/**
 * Created by IntelliJ IDEA.
 * User: zslajchrt
 * Date: 4.4.11
 * Time: 15:07
 * To change this template use File | Settings | File Templates.
 */

@SerialVersionUID(1L)
class Parameterizer(pageName: String) extends NamedConfigPage[Any](pageName, classOf[Parameterizer]) {
  type Cookie = ParameterizerConfig

  val getCookie = new ParameterizerConfig

  def consume(config: Cookie): ConfigResult[Any] = {
    val (paramCookie, paramMap) = constructParameters(config.paramDescriptors)
    val refEntry = config.project.getEntry(config.referredPage).asInstanceOf[TypedProjectEntry]
    NextPage(new ParametersCollectorPage(pageName + "#col", paramCookie, paramMap, config.referredPage, refEntry))
  }

  /**
   * The syntax of one parameter descriptor: paramName ',' paramClassName ',' pageId
   */
  private[memento] def constructParameters(paramDescriptors: java.util.List[String]): (Parameters, ParametersMap) = {
    var pageParamsMap: Map[String, String] = Map.empty // property -> page
    var paramsToParamMap: Map[String, String] = Map.empty // unique property -> property

    def getUniqueParamName(propName: String): String = {
      if (!pageParamsMap.contains(propName))
        propName
      else
        getUniqueParamName(propName + "$")
    }

//    val cookieGenerator = new BeanGenerator();
//    cookieGenerator.setSuperclass(classOf[Parameters]);

    val paramCookie = ExpressCookie.generateCookie(classOf[Parameters], cookieGenerator => {
      val descIter = paramDescriptors.iterator
      while (descIter.hasNext) {
        val propDesc = descIter.next
        val elems = propDesc.split(";")
        val paramName = elems(0)
        val propClassName = elems(1)
        val pageId = elems(2)

        val propClass = Thread.currentThread.getContextClassLoader.loadClass(propClassName)

        val uniqueParamName: String = getUniqueParamName(paramName)
        cookieGenerator.addProperty(uniqueParamName, propClass)

        pageParamsMap += (uniqueParamName -> pageId)
        paramsToParamMap += (uniqueParamName -> paramName)
      }

      cookieGenerator
    })

    ///

//    cookieGenerator.setUseCache(false)
//    cookieGenerator.createClass()
//    val classWriter = new ClassWriter(1)
//
//    var className: String = null
//    cookieGenerator.generateClass(new ClassAdapter(classWriter) {
//      override def visit(version: Int, access: Int, name: String, signature: String, superName: String, interfaces: Array[String]) = {
//        className = name.replace('/', '.')
//        super.visit(version, access, name, signature, superName, interfaces)
//      }
//    })
//
//    val byteCode = classWriter.toByteArray
//    val classLoader = new ClassLoader(Thread.currentThread.getContextClassLoader) {
//      override def findClass(name: String) = {
//        if (name == className)
//          defineClass(name, byteCode, 0, byteCode.length)
//        else null
//      }
//    }
//    val cls = classLoader.loadClass(className)
//    val paramCookie = cls.newInstance.asInstanceOf[Parameters]
//    ExpressCookie.initializeCookie(paramCookie, byteCode)

    ///

    val paramMap = new ParametersMap
    //val paramCookie = cookieGenerator.create.asInstanceOf[Parameters]
    paramMap.adjustPageParamsMap(pageParamsMap)
    paramMap.adjustUniqueParamToParamMap(paramsToParamMap)

    (paramCookie, paramMap)
  }

}

/**
 * This page is the first which follows the last page configured from persistent cookies.
 * Such pages should provide the type information about the product (HasProductType).
 */
@SerialVersionUID(1L)
class ParametersCollectorPage(pageName: String, parameters: Parameters, paramsMap: ParametersMap, refPageName: String,
                              refEntry: TypedProjectEntry)
  extends NamedConfigPage[Any](pageName, classOf[ParametersCollectorPage]) with HasProductType {

  //def id = classOf[ParametersCollectorPage].getName

  type Cookie = Parameters

  def getCookie() = parameters

  def getProductType() = refEntry.getEntryType

  private def injector(parameters: Parameters)(inputPage: ConfigPage[Any]): ConfigPage[Any] =
    new ParametersPopulator(inputPage, parameters, paramsMap)

  def consume(params: Cookie) = {
    val refPage = params.register.getConfigPage(refPageName, false, false, injector(params), Nil, true)
    NextPage(refPage)
  }

}

@SerialVersionUID(1L)
class ParametersPopulator[Any](val referredPage: ConfigPage[Any], parameters: Parameters, paramsMap: ParametersMap)
  extends ConfigPageDecorator[Any](referredPage) with Logged {

  override def consume(cookie: Cookie): ConfigResult[Any] = {
    if (cookie != Terminal)
      populateCookieWithParams(cookie)

    super.consume(cookie) match {
      case NextPage(nextPage) => NextPage(new ParametersPopulator(nextPage, parameters, paramsMap))
      case NewStarter(starter) => NewStarter(starter)
    }
  }

  private[memento] def populateCookieWithRecordedCookie(sourceCookie: AnyRef, targetCookie: AnyRef) {
    BeanUtils.copyBean(sourceCookie, targetCookie)
  }

  private[memento] def populateCookieWithParams(cookie: Cookie) {
    val sourceCookieBean = CookieBeanFactory(parameters)
    val targetCookieBean = CookieBeanFactory(cookie)

    val paramsForPage: Iterable[String] = paramsMap.getParametersForPage(referredPage.id)

    debug("Selected " + paramsForPage.size + " parameters for page " + referredPage.id)

    val updatedProps = paramsForPage.map(paramPropName => {
      val sourceProp = sourceCookieBean.property(paramPropName) match {
        case None => error(paramPropName + " property not found in source bean " + sourceCookieBean)
        case Some(p) => p
      }
      val realParamNameInPage = paramsMap.getRealParameterName(paramPropName)
      val targetProp = targetCookieBean.property(realParamNameInPage) match {
        case None => error(realParamNameInPage + " property not found in target bean " + targetCookieBean)
        case Some(p) => p
      }
      targetProp.update(sourceProp.value)
    })

    targetCookieBean.update(updatedProps.toList)
  }

}

@serializable
@SerialVersionUID(1L)
class ParameterizerConfig {
  var referredPage: String = null
//  /**
//   * Generic type expressed st the Java byte code signature.
//   * For example for type List<? extends Number> the signature is Ljava/util/List<+Ljava/lang/Number;>
//   */
//  var productType: String = null
  var paramDescriptors: java.util.List[String] = new ArrayList[String]

  @Ref(name = "?")
  var project: Project = null
}

object Parameters {
  def classToString(cls: Class[_]): String = {
    cls.getName match {
      case "byte" => "java.lang.Byte"
      case "short" => "java.lang.Short"
      case "int" => "java.lang.Integer"
      case "long" => "java.lang.Long"
      case "float" => "java.lang.Float"
      case "double" => "java.lang.Double"
      case "char" => "java.lang.Character"
      case "boolean" => "java.lang.Boolean"
      case _ => cls.getName
    }
  }
}

@serializable
@SerialVersionUID(1L)
class ParametersMap {
  // property -> page
  private var pageParamsMap: Map[String, String] = null
  // unique property -> property
  private var uniqueParamToParamMap: Map[String, String] = Map.empty

  def adjustPageParamsMap(pMap: Map[String, String]) {
    this.pageParamsMap = pMap
  }

  def adjustUniqueParamToParamMap(pMap: Map[String, String]) {
    this.uniqueParamToParamMap = pMap
  }


  def getRealParameterName(uniqueName: String): String =
    uniqueParamToParamMap(uniqueName)

  /**
   * It returns the unique (possibly generated) parameter names. The real names can be obtained by
   */
  def getParametersForPage(pageId: String): Iterable[String] =
    pageParamsMap.filter(_._2 == pageId).map(_._1)
}

/**
 * This cookie must be derived from ExpressCookie since it is used as the base for the GENERATED bean that holds
 * the parameters as its properties. Every ExpressCookie instance holds its byte code so that it can de-serialize
 * itself in an environment where its class has not been loaded yet.
 */
@SerialVersionUID(1L)
@JavaBean
class Parameters extends ExpressCookie {
  var register: ConfigPageRegister = null

  @Ref(name = "?") @NotNull
  def getRegister(): ConfigPageRegister = register

  def setRegister(reg: ConfigPageRegister) = register = reg

}
